import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:ui' as ui;

@immutable
class GradientBorderSide {
  /// Creates the side of a border.
  ///
  /// By default, the border is 1.0 logical pixels wide and solid black.
  const GradientBorderSide({
    this.gradient,
    this.width = 1.0,
    this.style = BorderStyle.solid,
  })  : assert(gradient != null),
        assert(width != null),
        assert(width >= 0.0),
        assert(style != null);

  /// Creates a [GradientBorderSide] that represents the addition of the two given
  /// [GradientBorderSide]s.
  ///
  /// It is only valid to call this if [canMerge] returns true for the two
  /// sides.
  ///
  /// If one of the sides is zero-width with [BorderStyle.none], then the other
  /// side is return as-is. If both of the sides are zero-width with
  /// [BorderStyle.none], then [GradientBorderSide.none] is returned.
  ///
  /// The arguments must not be null.
  static GradientBorderSide merge(GradientBorderSide a, GradientBorderSide b) {
    assert(a != null);
    assert(b != null);
    assert(canMerge(a, b));
    final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0;
    final bool bIsNone = b.style == BorderStyle.none && b.width == 0.0;
    if (aIsNone && bIsNone) return GradientBorderSide.none;
    if (aIsNone) return b;
    if (bIsNone) return a;
    assert(a.gradient == b.gradient);
    assert(a.style == b.style);
    return GradientBorderSide(
      gradient: a.gradient, // == b.color
      width: a.width + b.width,
      style: a.style, // == b.style
    );
  }

  final Gradient gradient;

  /// The width of this side of the border, in logical pixels.
  ///
  /// Setting width to 0.0 will result in a hairline border. This means that
  /// the border will have the width of one physical pixel. Also, hairline
  /// rendering takes shortcuts when the path overlaps a pixel more than once.
  /// This means that it will render faster than otherwise, but it might
  /// double-hit pixels, giving it a slightly darker/lighter result.
  ///
  /// To omit the border entirely, set the [style] to [BorderStyle.none].
  final double width;

  /// The style of this side of the border.
  ///
  /// To omit a side, set [style] to [BorderStyle.none]. This skips
  /// painting the border, but the border still has a [width].
  final BorderStyle style;

  /// A hairline black border that is not rendered.
  static const GradientBorderSide none = GradientBorderSide(
      width: 0.0,
      style: BorderStyle.none,
      gradient: LinearGradient(colors: []));

  /// Creates a copy of this border but with the given fields replaced with the new values.
  GradientBorderSide copyWith({
    Color color,
    double width,
    BorderStyle style,
  }) {
    assert(width == null || width >= 0.0);
    return GradientBorderSide(
      gradient: color ?? this.gradient,
      width: width ?? this.width,
      style: style ?? this.style,
    );
  }

  /// Creates a copy of this border side description but with the width scaled
  /// by the factor `t`.
  ///
  /// The `t` argument represents the multiplicand, or the position on the
  /// timeline for an interpolation from nothing to `this`, with 0.0 meaning
  /// that the object returned should be the nil variant of this object, 1.0
  /// meaning that no change should be applied, returning `this` (or something
  /// equivalent to `this`), and other values meaning that the object should be
  /// multiplied by `t`. Negative values are treated like zero.
  ///
  /// Since a zero width is normally painted as a hairline width rather than no
  /// border at all, the zero factor is special-cased to instead change the
  /// style to [BorderStyle.none].
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  GradientBorderSide scale(double t) {
    assert(t != null);
    return GradientBorderSide(
      gradient: gradient,
      width: math.max(0.0, width * t),
      style: t <= 0.0 ? BorderStyle.none : style,
    );
  }

  /// Create a [Paint] object that, if used to stroke a line, will draw the line
  /// in this border's style.
  ///
  /// Not all borders use this method to paint their border sides. For example,
  /// non-uniform rectangular [Border]s have beveled edges and so paint their
  /// border sides as filled shapes rather than using a stroke.
  Paint toPaint() {
    switch (style) {
      case BorderStyle.solid:
        return Paint()
          ..shader = gradient.createShader(Rect.zero)
          ..strokeWidth = width
          ..style = PaintingStyle.stroke;
      case BorderStyle.none:
        return Paint()
          ..color = const Color(0x00000000)
          ..strokeWidth = 0.0
          ..style = PaintingStyle.stroke;
      default:
        return Paint();
    }
  }

  /// Whether the two given [GradientBorderSide]s can be merged using [new
  /// BorderSide.merge].
  ///
  /// Two sides can be merged if one or both are zero-width with
  /// [BorderStyle.none], or if they both have the same color and style.
  ///
  /// The arguments must not be null.
  static bool canMerge(GradientBorderSide a, GradientBorderSide b) {
    assert(a != null);
    assert(b != null);
    if ((a.style == BorderStyle.none && a.width == 0.0) ||
        (b.style == BorderStyle.none && b.width == 0.0)) return true;
    return a.style == b.style && a.gradient == b.gradient;
  }

  /// Linearly interpolate between two border sides.
  ///
  /// The arguments must not be null.
  ///
  /// {@macro dart.ui.shadow.lerp}
  static GradientBorderSide lerp(
      GradientBorderSide a, GradientBorderSide b, double t) {
    assert(a != null);
    assert(b != null);
    assert(t != null);
    if (t == 0.0) return a;
    if (t == 1.0) return b;
    final double width = ui.lerpDouble(a.width, b.width, t);
    if (width < 0.0) return GradientBorderSide.none;
    if (a.style == b.style) {
      return GradientBorderSide(
        gradient: Gradient.lerp(a.gradient, b.gradient, t),
        width: width,
        style: a.style, // == b.style
      );
    }
    Gradient colorA, colorB;
    switch (a.style) {
      case BorderStyle.solid:
        colorA = a.gradient;
        break;
      case BorderStyle.none:
        colorA = LinearGradient(colors: []);
        break;
    }
    switch (b.style) {
      case BorderStyle.solid:
        colorB = b.gradient;
        break;
      case BorderStyle.none:
        colorB = LinearGradient(colors: []);
        break;
    }
    return GradientBorderSide(
      gradient: Gradient.lerp(colorA, colorB, t),
      width: width,
      style: BorderStyle.solid,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other.runtimeType != runtimeType) return false;
    return other is GradientBorderSide &&
        other.gradient == gradient &&
        other.width == width &&
        other.style == style;
  }

  @override
  int get hashCode => hashValues(gradient, width, style);

  @override
  String toString() =>
      '${objectRuntimeType(this, 'BorderSide')}($gradient, ${width.toStringAsFixed(1)}, $style)';
}
